#include "stdafx.h"

#include "Terrain.h"
#include "graphics/Texture.h"
#include "state/RunningState.h"
#include "graphics/Shader.h"
#include "core/game.h"
#include "terrain/PerlinNoise.h"

Terrain* Terrain::instance = nullptr;
Terrain* Terrain::getInstance() 
{
    if ( !instance ) 
        instance = new Terrain();
    return instance;
}


Terrain::Terrain()
{
	initShader();
	initGeometry();
}

void Terrain::initGeometry()
{
	m_nVertsHeight = 512;
	m_nVertsWidth = 512;
	width = 512.0f;
	height = 512.0f;
	
	PerlinNoise perlinNoise(0.45f, 16.0f/*freq*/, 35.0f/*ampl*/, 8/*octaves*/, 200);

	double bumpiness = 1.0f;
	glm::vec2 spacing((float)width / m_nVertsWidth, (float)height / m_nVertsHeight);
	{ // create geometry 
		for ( size_t i = 0; i < m_nVertsHeight; ++i ) 
		for ( size_t j = 0; j < m_nVertsWidth; ++j )
		{
#if 0  
			if ( i > 5 && i < 15) {
				heights.push_back(100.0f/255.0f);
			}
			
#endif 
			//heights.push_back(/*-10.0f*/ 5.0f*cos(0.15f*(float)i) );
			//heights.push_back(/*-10.0f*/ 0.0f );
			heights.push_back(perlinNoise.Get( glm::vec2(j/(double)m_nVertsWidth, i/(double)m_nVertsHeight) ));
#if 0
			float h = 0.0f; // height
			float w = 0.5f; // octave weight
			float m = 0.4f; // octave multiplier
			for (int k=1; k<8; k++) {
				h += 10.0f * w * simpleNoise(glm::vec2(k*k*j*0.025f,k*k*i*0.025f));
				w *= 0.5;
				m *= 2.0;
			}
			heights.push_back(h);
#endif 
			//heights.push_back(0.15f*i);
		}
        
		heightTex = PingPongTexture(glm::ivec2(m_nVertsWidth, m_nVertsHeight), GL_R16F, GL_RED, GL_FLOAT, GL_LINEAR, GL_CLAMP_TO_EDGE, &heights[0]);
        //heightFBO.attach(heightTex.get(), GL_COLOR_ATTACHMENT0);
        normalTex = Texture(glm::ivec2(m_nVertsWidth, m_nVertsHeight), GL_RGBA16F, GL_RGBA, GL_FLOAT, GL_LINEAR, GL_CLAMP_TO_EDGE, nullptr);
		//normalFBO.attach(normalTex.get(), GL_COLOR_ATTACHMENT0);
		
        for ( size_t i = 0; i < m_nVertsHeight; ++i )
		for ( size_t j = 0; j < m_nVertsWidth; ++j )
		{
			glm::vec2 normPos((float)(j)/(m_nVertsWidth-1), (float)(i)/(m_nVertsHeight-1));
			glm::vec2 pos = glm::vec2(normPos.x * width, normPos.y * height);

            //glm::vec2 texelPos(((0.5f+j)/nVertsWidth), ((0.5f+i)/nVertsHeight));
            glm::vec2 texelPos(static_cast<float>(j)/(m_nVertsWidth-1), static_cast<float>(i)/(m_nVertsHeight-1));

			vertices.push_back( PTVertex(glm::vec3(pos.x, 0.0f, pos.y), texelPos));
		}
		
		for ( size_t i = 0; i < m_nVertsHeight-1; ++i )
		for ( size_t j = 0; j < m_nVertsWidth-1; ++j )
		{
			indices.push_back((j+1)	+ (i+1)*m_nVertsWidth);
			indices.push_back((j+1) + i*m_nVertsWidth);
			indices.push_back(j		+ i*m_nVertsWidth);

			indices.push_back((j)	+ (i+1)*m_nVertsWidth);
			indices.push_back((j+1) + (i+1)*m_nVertsWidth);
			indices.push_back(j		+ i*m_nVertsWidth);
		}
	}

	{ // bind geometry to buffers 
		glGenBuffers(1, &vbo);
		glGenBuffers(1, &ibo);
		glGenVertexArrays(1, &vao);

		glBindBuffer(GL_ARRAY_BUFFER, vbo);
		{
			// vertices
			glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(PTVertex), &vertices[0], GL_STATIC_DRAW);
		} glBindBuffer(GL_ARRAY_BUFFER, 0);
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
		{
			// indices
			glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), &indices[0], GL_STATIC_DRAW);
		} glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

		glBindVertexArray(vao);
		{
			glBindBuffer(GL_ARRAY_BUFFER, vbo);
			{
				// vertex layout, offset for the different attribs
				unsigned int offset = 0;
				glVertexAttribPointer(semantic::attr::POSITION, 3, GL_FLOAT, GL_FALSE, sizeof(PTVertex), BUFFER_OFFSET(offset));
				offset += sizeof(glm::vec3);
				glVertexAttribPointer(semantic::attr::TEXCOORD, 2, GL_FLOAT, GL_FALSE, sizeof(PTVertex), BUFFER_OFFSET(offset));
			} 
			glEnableVertexAttribArray(semantic::attr::POSITION);
			glEnableVertexAttribArray(semantic::attr::TEXCOORD);
		} glBindVertexArray(0);
	} 

	screenQuad();
	computeNormals();

	// process heights 

	::flushGLError("Terrain::initGeometry()");
}

void Terrain::initShader()
{
	defaultShader = new Shader();
	defaultShader->addStage("./graphics/shaders/terrainSimple.vert", "", GL_VERTEX_SHADER);
	defaultShader->addStage("./graphics/shaders/terrainSimple.frag", "", GL_FRAGMENT_SHADER);
	defaultShader->install();

	pickingShader = new Shader();
	pickingShader->addStage("./graphics/shaders/pickingTerrain.vert", "", GL_VERTEX_SHADER);
	pickingShader->addStage("./graphics/shaders/pickingTerrain.frag", "", GL_FRAGMENT_SHADER);
	pickingShader->install();

	normalShader = new Shader();
	normalShader->addStage("./graphics/shaders/fullscreen.vert", "", GL_VERTEX_SHADER);
	normalShader->addStage("./graphics/shaders/computeNormals.frag", "", GL_FRAGMENT_SHADER);
	normalShader->install();

	smoothShader = new Shader();
	smoothShader->addStage("./graphics/shaders/fullscreen.vert", "", GL_VERTEX_SHADER);
	smoothShader->addStage("./graphics/shaders/smooth.frag", "", GL_FRAGMENT_SHADER);
	smoothShader->install();
}

void Terrain::renderTerrain(Shader* renderShader, const glm::mat4& viewMatrix)
{
	renderShader->begin();
	{
		int viewLoc = renderShader->getUniLoc("view");
		int projLoc = renderShader->getUniLoc("proj");
        int texDimLoc = renderShader->getUniLoc("texDim");

		glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(viewMatrix));
		glUniformMatrix4fv(projLoc, 1, GL_FALSE, glm::value_ptr(Game::getPerspMatrix()));
        glUniform2iv(texDimLoc, 1, glm::value_ptr(heightTex.getDim()));

		int texResLoc = renderShader->getUniLoc("texRes");
		glUniform2iv(texResLoc, 1, glm::value_ptr(heightTex.getDim()));

		glActiveTexture(GL_TEXTURE0);
		glBindTexture(GL_TEXTURE_2D, heightTex.Out());
        
		glUniform1i(renderShader->getUniLoc("heightSampler"), 0);

		glActiveTexture(GL_TEXTURE1);
		normalTex.bind();

		glUniform1i(renderShader->getUniLoc("normalSampler"), 1);

		glBindVertexArray(vao);
		{
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ibo);
            glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
		} glBindVertexArray(0);
	} renderShader->end();

	::flushGLError("Terrain::renderTerrain()");
}

float Terrain::sample(const glm::vec2 & pos)
{
	//return -10.0f;
#if 1 // slow currently, fix later 
	heightFBO.attach(heightTex.In(), GL_COLOR_ATTACHMENT0);
    glm::vec4 pixel = heightFBO.sample( glm::dvec2(static_cast<int>(pos.x * m_nVertsWidth / width), static_cast<int>(pos.y * m_nVertsHeight / height)) );
    return pixel.x;
#endif 
}

glm::vec3 Terrain::gradient(const glm::vec2 & pos)
{
	glm::dvec2 vec0(static_cast<int>(pos.x * m_nVertsWidth / width), static_cast<int>(pos.y * m_nVertsHeight / height));

	if (vec0.x < 0.0f || vec0.y < 0.0f || vec0.x >= m_nVertsWidth || vec0.y >= m_nVertsHeight) return glm::vec3(0.0f, 0.0f, 0.0f);

	float h00 = heightFBO.sample(vec0).x;
	
	glm::dvec2 vec01 = vec0 + glm::dvec2(-1.0f, 0.0f);
	glm::dvec2 vec21 = vec0 + glm::dvec2(1.0f, 0.0f);
	if (vec01.x < 0 || vec21.x >= m_nVertsWidth) return glm::vec3(0.0f, 0.0f, 0.0f);

	glm::dvec2 vec10 = vec0 + glm::dvec2(0.0f, -1.0f);
	glm::dvec2 vec12 = vec0 + glm::dvec2(0.0f, 1.0f);
	if (vec10.y < 0 || vec12.y >= m_nVertsHeight) return glm::vec3(0.0f, 0.0f, 0.0f);

	float h01 = heightFBO.sample(vec01).x;
	float h21 = heightFBO.sample(vec21).x;

	float h10 = heightFBO.sample(vec10).x;
	float h12 = heightFBO.sample(vec12).x;
	
	/*glm::dvec2 vec11(static_cast<int>(1 + pos.x * nVertsWidth / width), static_cast<int>(1 + pos.y * nVertsHeight / height));
	float h11 = heightFBO.sample(vec11).x;*/

	float d0 = fabs(h21 - h01);
	float d1 = fabs(h12 - h10);

	return glm::vec3(d0*d0+d1*d1, 0.0f, 0.0f); // TODO !
}

glm::vec2 Terrain::pick(const glm::vec4& pixel)
{
    if ( pixel.z > 0.0 || pixel.w > 0.0 ) 
        return glm::vec2(-1.0f, -1.0f);
    return glm::vec2(width*pixel.x, height*pixel.y);
}

// TODO: use a diff shader when not render to FBOs
void Terrain::render(const glm::mat4& viewMatrix, bool picking)
{
    if (!picking) {
    //    glPolygonMode( GL_FRONT_AND_BACK, GL_LINE );
        renderTerrain(defaultShader, viewMatrix);
    //    glPolygonMode( GL_FRONT_AND_BACK, GL_FILL );
    } else {
        renderTerrain(pickingShader, viewMatrix);
    }

	static int local_counter = 0;
	local_counter++;
	if (local_counter % 5 == 0)
		smoothTerrain();
	computeNormals();
}

void Terrain::screenQuad() {
	std::vector<glm::vec2> quadVertices;

	glGenVertexArrays(1, &quadVAO);
	quadVertices.push_back(glm::vec2(-1.0f, -1.0f));
	quadVertices.push_back(glm::vec2(1.0f, -1.0f));
	quadVertices.push_back(glm::vec2(1.0f, 1.0f));
	quadVertices.push_back(glm::vec2(-1.0f, 1.0f));

	glGenBuffers(1, &quadBuffer);
	glBindBuffer(GL_ARRAY_BUFFER, quadBuffer);
	{
		glBufferData(GL_ARRAY_BUFFER, quadVertices.size() * sizeof(glm::vec2), &quadVertices[0], GL_STATIC_DRAW);
	}

	glBindVertexArray(quadVAO);
	{
		glBindBuffer(GL_ARRAY_BUFFER, quadBuffer);
		{
			glVertexAttribPointer(semantic::attr::POSITION, 2, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));
		}
		glEnableVertexAttribArray(semantic::attr::POSITION);
	}

::flushGLError("Terrain::screenQuad()");
}

void Terrain::computeNormals() {

	normalFBO.attach(normalTex.get(), GL_COLOR_ATTACHMENT0);
	normalFBO.bind();
	
	glClearBufferfv(GL_COLOR, 0, glm::value_ptr(glm::vec4(0.0f, 0.0f, 0.0f, 0.0f)));
	glViewport(0, 0, m_nVertsWidth, m_nVertsHeight);
	normalShader->begin();
	{
		int texResLoc = normalShader->getUniLoc("texRes");
		glUniform2iv(texResLoc, 1, glm::value_ptr(heightTex.getDim()));

		glActiveTexture(GL_TEXTURE0);

		// compute normals from the current height texture
		glBindTexture(GL_TEXTURE_2D, heightTex.Out());

		glUniform1i(normalShader->getUniLoc("heightSampler"), 0);

		glBindVertexArray(quadVAO);
        {
            glDrawArrays(GL_QUADS, 0, 4);
        } glBindVertexArray(0);

	} normalShader->end();

	normalFBO.unbind();
	glViewport(0, 0, static_cast<GLsizei>(Game::windowDim.x), static_cast<GLsizei>(Game::windowDim.y));

	::flushGLError("Terrain::computeNormals()");
}

void Terrain::smoothTerrain() {

	//write target 
	heightFBO.attach(heightTex.Out(), GL_COLOR_ATTACHMENT0);
	heightFBO.bind();

	glClearBufferfv(GL_COLOR, 0, glm::value_ptr(glm::vec4(0.0f, 0.0f, 0.0f, 0.0f)));
	glViewport(0, 0, m_nVertsWidth, m_nVertsHeight);
	smoothShader->begin();
	{
		int texResLoc = smoothShader->getUniLoc("texRes");
		glUniform2iv(texResLoc, 1, glm::value_ptr(heightTex.getDim()));

		glActiveTexture(GL_TEXTURE0);
		
		glBindTexture(GL_TEXTURE_2D, heightTex.In());

		glUniform1i(smoothShader->getUniLoc("inSampler"), 0);

		glBindVertexArray(quadVAO);
		{
			glDrawArrays(GL_QUADS, 0, 4);
		} glBindVertexArray(0);

	} smoothShader->end();

	heightFBO.unbind();
	glViewport(0, 0, static_cast<GLsizei>(Game::windowDim.x), static_cast<GLsizei>(Game::windowDim.y));

	heightTex.swap();

	::flushGLError("Terrain::computeNormals()");
}